package br.unisinos.servers;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import br.lp2p.commons.LP2PInternalMessages;
import br.unisinos.arquivos.DirectoryEntry;
import br.unisinos.arquivos.FileShared;
import br.unisinos.arquivos.RemoteFiles;

/*
 * 
 * Class Server
 * This class waits to be contacted by one of the clients [kernel]
 * 
 */
public class Server implements Runnable {

	private List<DirectoryEntry> entries;
	private List<String> machinesAddress;// {"192.168.0.1"};
	private Map<String, RemoteFiles> filesLocation;

	public Server(List<DirectoryEntry> entries) {
		this.entries = entries;

		loadPeersFromFile();

		filesLocation = new HashMap<String, RemoteFiles>();
		for (String machineAddress : machinesAddress) {
			RemoteFiles remFiles = new RemoteFiles(-1, "");
			filesLocation.put(machineAddress, remFiles);
		}
	}

	public void loadPeersFromFile() {
		List<String> localAddresses = getThisMachineAddresses();
		List<String> addresses = new ArrayList<String>();
		try {
			BufferedReader reader = new BufferedReader(new FileReader("peers.txt"));
			String fileLine = null;
			while ((fileLine = reader.readLine()) != null) {
				if (fileLine.equals("")) {
					continue;
				}
				boolean isAddrLocal = false;

				for (String address : localAddresses) {
					if (address.equals(fileLine)) {
						isAddrLocal = true;
						break;
					}
				}
				if (!isAddrLocal) {
					addresses.add(fileLine);
				}
			}
			reader.close();
		} catch (IOException ioex) {
			ioex.printStackTrace();
		}

		// addresses.add("127.0.0.1");
		machinesAddress = addresses;
		
		// for (int j=0; j<machinesAddress.length; j++)
		// System.out.println(machinesAddress[j]);
		// System.out.println("---");
	}

	public List<String> getThisMachineAddresses() {
		List<String> addresses = new ArrayList<String>();

		Enumeration<NetworkInterface> nis = null;
		try {
			nis = NetworkInterface.getNetworkInterfaces();
		} catch (SocketException e) {
			System.err.println("Error getting network interfaces");
			e.printStackTrace();
		}
		if (nis != null) {
			while (nis.hasMoreElements()) {
				NetworkInterface ni = (NetworkInterface) nis.nextElement();
				Enumeration<InetAddress> ias = ni.getInetAddresses();
				while (ias.hasMoreElements()) {
					InetAddress ia = (InetAddress) ias.nextElement();
					addresses.add(ia.getHostAddress());
				}
			}
		}
		return addresses;
	}

	@Override
	public void run() {
		waitMessage();
	}

	public void waitMessage() {
		ServerSocket servSock;
		Socket clntSock = null;
		byte[] byteBuffer; // Receive buffer

		try {
			servSock = new ServerSocket(LP2PInternalMessages.SERVER_PORT);

			while (true) {
				byteBuffer = new byte[LP2PInternalMessages.MSG_BUFLEN];
				clntSock = servSock.accept();
				clntSock.setSoTimeout(5000);
				clntSock.setSendBufferSize(LP2PInternalMessages.SENDF_BUFLEN);

				// System.out.println("Handling client at " +
				// clntSock.getInetAddress().getHostAddress() + " on port " +
				// clntSock.getPort());

				try {
					InputStream in = clntSock.getInputStream();
					OutputStream out = clntSock.getOutputStream();

					int numBytes = in.read(byteBuffer);
					// System.out.println(new String(byteBuffer));
					byte[] resp = processMessage(byteBuffer, numBytes);
					// System.out.println(resp.length);
					out.write(resp);
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					try {
						clntSock.close();
					} catch (Exception e) {
					}
				}
			}
		}

		catch (IOException ioex) {
			ioex.printStackTrace();
		}

	}

	public byte[] processMessage(byte[] byteBuffer, int numBytes) {
		byte[] resp = null;
		int respLen;
		StringBuilder tmpResp = new StringBuilder();
		ByteBuffer bbuf = null;

		// for (int i=0; i<4; i++)
		// protocol.append((char) byteBuffer[i]);

		byte op = byteBuffer[6];

		switch (op) {
		case LP2PInternalMessages.LP2P_LLIST:
			// received a list message
			int i = 0;

			// to send to others LP2P
			StringBuilder remoteFiles = getRemoteFiles(byteBuffer, numBytes);

			// ismael: busca o nome do compartilhamento e coloca em tmpResp
			for (i = 7; i < byteBuffer.length; i++) {
				char tmp = (char) byteBuffer[i];
				if (tmp != LP2PInternalMessages.DELIM)
					tmpResp.append(tmp);
				else
					break;
			}
			// ismael: debug: tmpResp = [compartilhamento]

			// generate response
			String files = null;
			int numLocalFiles = 0;
			for (DirectoryEntry entry : entries) {
				if (tmpResp.toString().equals(entry.getRelativeDirName())) {
					files = entry.getFormatedShare(); // ismael:files recebe a
														// lista dos arquivos e
														// suas propriedades
														// separados pelo
														// espacador.
					numLocalFiles = entry.getNumFiles();
					break;
				}
			}

			StringBuilder numRemoteFilesStr = new StringBuilder();
			// for (int j=0; j<remoteFiles.length(); j++){
			while (remoteFiles.charAt(0) != LP2PInternalMessages.DELIM) {
				// if (remoteFiles.charAt(0) == Server.DELIM){
				// remoteFiles.deleteCharAt(0);
				// break;
				// }
				// else {
				numRemoteFilesStr.append(remoteFiles.charAt(0));
				remoteFiles.deleteCharAt(0); // ismael: possivel otimizacao: nao
												// deletar o char, apenas
												// guardar a posicao. Deixar o
												// GC ao fim do processamento
												// recolher o obj.
				// }
			}

			if (remoteFiles.charAt(0) == LP2PInternalMessages.DELIM) {
				remoteFiles.deleteCharAt(0);
			}

			int numRemoteFiles = Integer.parseInt(numRemoteFilesStr.toString());
			tmpResp.append(LP2PInternalMessages.DELIM);
			// ismael: debug: tmpResp = [compartilhamento]\
			if (numRemoteFiles > 0) {
				tmpResp.append((numLocalFiles + numRemoteFiles));
			} else {
				tmpResp.append(numLocalFiles);
			}
			tmpResp.append(LP2PInternalMessages.DELIM);
			// ismael: debug: tmpResp = [compartilhamento]\[numfiles]\
			if (files != null)
				tmpResp.append(files);
			// ismael: debug: tmpResp =
			// [compartilhamento]\[numfiles]\[listaDeArquivos]
			bbuf = ByteBuffer.allocateDirect(7 + tmpResp.length() + remoteFiles.length());
			bbuf.put("LP2P".getBytes());
			bbuf.put((byte) 1);
			bbuf.put((byte) 1);
			bbuf.put(LP2PInternalMessages.LP2P_LSENDL);
			bbuf.put(tmpResp.toString().getBytes());
			// System.out.println("Num local files: " + numLocalFiles +
			// " num remote files: " + numRemoteFiles);
			// System.out.println("remote files:" + remoteFiles.toString());
			// System.out.println("Files:" + tmpResp.toString() + "\n-----\n" +
			// remoteFiles.toString() + "\n" + (tmpResp.length() +
			// remoteFiles.length()));

			if (numRemoteFiles > 0) {
				bbuf.put(remoteFiles.toString().getBytes());
			}

			break;
		case LP2PInternalMessages.LP2P_LGET:
			// received a get message
			int j = 0;
			int k = 0;
			int dataLen = 0;
			long fileId;
			long offset;
			long length;
			byte[] data = null;
			StringBuilder sb = new StringBuilder();

			// get sharing name
			for (j = 7; j < byteBuffer.length; j++) {
				char tmp = (char) byteBuffer[j];
				if (tmp != LP2PInternalMessages.DELIM)
					tmpResp.append(tmp);
				else
					break;
			}

			// get fileID
			j++;
			for (k = j; k < byteBuffer.length; k++) {
				char tmp = (char) byteBuffer[k];
				if (tmp != LP2PInternalMessages.DELIM)
					sb.append(tmp);
				else
					break;
			}
			fileId = Long.parseLong(sb.toString());

			// get offset
			j = k + 1;
			sb = new StringBuilder();
			for (k = j; k < byteBuffer.length; k++) {
				char tmp = (char) byteBuffer[k];
				if (tmp != LP2PInternalMessages.DELIM)
					sb.append(tmp);
				else
					break;
			}
			offset = Long.parseLong(sb.toString());

			// get length
			j = k + 1;
			sb = new StringBuilder();
			for (k = j; k < byteBuffer.length; k++) {
				char tmp = (char) byteBuffer[k];
				if (tmp != LP2PInternalMessages.DELIM)
					sb.append(tmp);
				else
					break;
			}
			length = Long.parseLong(sb.toString());
			// System.out.println("length=" + length);

			FileShared file = null;
			for (DirectoryEntry entry : entries) {
				if (tmpResp.toString().equals(entry.getRelativeDirName())) {
					file = entry.getSelectedFile((int) fileId);
					break;
				}
			}

			if (file != null) {
				data = file.getData(offset, length);
				if (data != null)
					dataLen = data.length;
				else
					dataLen = 0;

				// generate response
				tmpResp.append(LP2PInternalMessages.DELIM);
				tmpResp.append(fileId);
				tmpResp.append(LP2PInternalMessages.DELIM);
				tmpResp.append(offset);
				tmpResp.append(LP2PInternalMessages.DELIM);
				tmpResp.append(dataLen);
				tmpResp.append(LP2PInternalMessages.DELIM);

				// System.out.println("Answer without data: " +
				// tmpResp.toString());
				respLen = 7 + tmpResp.length() + dataLen;

				bbuf = ByteBuffer.allocateDirect(respLen);
				bbuf.put("LP2P".getBytes());
				bbuf.put((byte) 1);
				bbuf.put((byte) 1);
				bbuf.put(LP2PInternalMessages.LP2P_LSENDF);
				bbuf.put(tmpResp.toString().getBytes());

				if (dataLen > 0)
					bbuf.put(data);
			} else {
				// it's a remote file
				// to send to others LP2P
				// System.out.println("It's a remote file");
				byte[] tmpData = getRemoteFileData(byteBuffer, numBytes, fileId, length);

				if (tmpData == null) {
					tmpResp.append(LP2PInternalMessages.DELIM);
					tmpResp.append(fileId);
					tmpResp.append(LP2PInternalMessages.DELIM);
					tmpResp.append(offset);
					tmpResp.append(LP2PInternalMessages.DELIM);
					tmpResp.append(0);
					tmpResp.append(LP2PInternalMessages.DELIM);

					// System.out.println(tmpResp.toString());

					bbuf = ByteBuffer.allocateDirect(7 + tmpResp.length());
					bbuf.put("LP2P".getBytes());
					bbuf.put((byte) 1);
					bbuf.put((byte) 1);
					bbuf.put(LP2PInternalMessages.LP2P_LSENDF);
					bbuf.put(tmpResp.toString().getBytes());
				} else {
					return tmpData;
				}
			}

			break;
		case LP2PInternalMessages.LP2P_LGETU:
			for (i = 7; i < byteBuffer.length; i++) {
				char tmp = (char) byteBuffer[i];
				if (tmp != LP2PInternalMessages.DELIM)
					tmpResp.append(tmp);
				else
					break;
			}
			StringBuilder sbb2 = new StringBuilder();
			/*
			 * tmpResp.append(Server.DELIM); tmpResp.append(0);
			 * tmpResp.append(Server.DELIM); tmpResp.append(0);
			 * tmpResp.append(Server.DELIM);
			 * 
			 * sbb2.append("arq4000M_3"); sbb2.append(Server.DELIM);
			 * sbb2.append(0); sbb2.append(Server.DELIM); sbb2.append(0);
			 * sbb2.append(Server.DELIM);
			 * 
			 * bbuf = ByteBuffer.allocateDirect(7 + tmpResp.length() + 1 +
			 * sbb2.length()); bbuf.put("LP2P".getBytes()); bbuf.put((byte) 1);
			 * bbuf.put((byte) 1); bbuf.put(LP2P_LSENDU);
			 * bbuf.put(tmpResp.toString().getBytes()); bbuf.put((byte) 1);
			 * bbuf.put(sbb2.toString().getBytes());
			 */

			tmpResp.append(LP2PInternalMessages.DELIM);
			tmpResp.append(0);
			tmpResp.append(LP2PInternalMessages.DELIM);

			bbuf = ByteBuffer.allocateDirect(7 + tmpResp.length() + 1 + sbb2.length());
			bbuf.put("LP2P".getBytes());
			bbuf.put((byte) 1);
			bbuf.put((byte) 1);
			bbuf.put(LP2PInternalMessages.LP2P_LSENDU);
			bbuf.put(tmpResp.toString().getBytes());

			// System.out.println("Files:" + tmpResp.toString());

			break;
		}
		bbuf.flip();
		resp = new byte[bbuf.limit()];
		bbuf.get(resp);
		return resp;
	}

	public StringBuilder getRemoteFiles(byte[] bufToSend, int numBytes) {
		StringBuilder remoteFiles = new StringBuilder("");
		int totalBytes;
		int numRemoteFiles = 0;

		for (String machineAddress : machinesAddress) {
			RemoteFiles rFiles = filesLocation.get(machineAddress);

			if (rFiles.getNumFiles() == -1) {
				// need to get that machine files
				try {
					Socket clientSocket;

					clientSocket = new Socket(machineAddress, TcpServer.TCP_PORT);
					clientSocket.setReceiveBufferSize(LP2PInternalMessages.SENDF_BUFLEN);

					DataInputStream in = new DataInputStream(clientSocket.getInputStream());
					// InputStream in = clientSocket.getInputStream();
					OutputStream out = clientSocket.getOutputStream();

					out.write(bufToSend, 0, numBytes);

					byte[] data = new byte[LP2PInternalMessages.SENDL_BUFLEN];

					totalBytes = 0;
					int numRead = 0;
					do {
						numRead = in.read(data, totalBytes, data.length - totalBytes);
						if (numRead > 0)
							totalBytes += numRead;
					} while ((totalBytes < LP2PInternalMessages.SENDL_BUFLEN) && (numRead > 0));

					// to get the index of the first file
					int j = 7;
					while (((char) data[j]) != LP2PInternalMessages.DELIM) {
						j++;
					}
					j++;

					// number of files
					StringBuilder num = new StringBuilder();
					while (((char) data[j]) != LP2PInternalMessages.DELIM) {
						num.append((char) data[j]);
						j++;
					}
					j++;
					int num2 = Integer.parseInt(num.toString());

					if (num2 > 0) {
						// there are files
						String tmp = new String(data, j, totalBytes - j);
						RemoteFiles remFiles = new RemoteFiles(num2, tmp);
						filesLocation.remove(machineAddress);
						filesLocation.put(machineAddress, remFiles);
						numRemoteFiles += num2;
						remoteFiles.append(tmp);
					} else {
						RemoteFiles remFiles = new RemoteFiles(0, "");
						filesLocation.put(machineAddress, remFiles);
					}

					clientSocket.close();
				} catch (Exception e) {
					// if an error occurred, we couldn't get
					// any file from the remote LP2P
					// so the default value remains ("")
					e.printStackTrace();
				}

			} else {
				// we already have the list of files
				if (rFiles.getNumFiles() > 0) {
					numRemoteFiles += rFiles.getNumFiles();
					remoteFiles.append(rFiles.getFiles());
				}
			}
		}

		StringBuilder totalData = new StringBuilder();
		totalData.append(numRemoteFiles);
		totalData.append(LP2PInternalMessages.DELIM);
		totalData.append(remoteFiles.toString());
		// System.out.println(totalData);
		return totalData;
	}

	public byte[] getRemoteFileData(byte[] bufToSend, int numBytes, long fileId, long minLength) {
		byte[] resp = null;
		int totalBytes = 0;
		StringBuilder searchStr = new StringBuilder("");
		searchStr.append(fileId);
		searchStr.append(LP2PInternalMessages.DELIM);
		searchStr.append(LP2PInternalMessages.DELIM);

		for (String machineAddress : machinesAddress) {
			String rFiles = filesLocation.get(machineAddress).getFiles();

			Pattern pattern = Pattern.compile(searchStr.toString());
			Matcher matcher = pattern.matcher(rFiles);
			if (matcher.find()) {
				// need to get the data of the file
				Socket clientSocket;
				resp = new byte[LP2PInternalMessages.SENDF_BUFLEN];

				try {
					clientSocket = new Socket(machineAddress, TcpServer.TCP_PORT);
					clientSocket.setReceiveBufferSize(LP2PInternalMessages.SENDF_BUFLEN);

					InputStream in = clientSocket.getInputStream();
					OutputStream out = clientSocket.getOutputStream();

					out.write(bufToSend, 0, numBytes);

					totalBytes = 0;
					int count = 0;
					do {
						count = in.read(resp, totalBytes, 60000);
						if (count > 0)
							totalBytes += count;
					} while ((totalBytes < minLength + 10) && (totalBytes < LP2PInternalMessages.SENDF_BUFLEN) && (count > 0));

					// System.out.println("Bytes received=" + totalBytes);

					ByteBuffer buf2 = ByteBuffer.allocateDirect(LP2PInternalMessages.SENDF_BUFLEN);
					buf2.put(resp, 0, totalBytes);
					buf2.flip();
					resp = new byte[buf2.limit()];
					buf2.get(resp);

					clientSocket.close();
				} catch (Exception e) {
					resp = null;
					e.printStackTrace();
				}
				break;
			}
		}
		return resp;
	}
}
